#!pip install "pandas<2.0.0"
#!pip install plotly
#!pip install folium
#!pip install geopy
#!pip install nbformat Auswertung von Daten der deutschen Bahn
Einleitung
Schienennetz vor dem Kollaps - so titeln Marie Blöcher, Nils Naber und Isabel Schneider vom NDR. Die Deutsche Bahn habe zwar ehrgeizige Ziele, allerdings ist jahrelang zu wenig Geld ins Netz geflossen.
Rund 60 Milliarden Euro müssten laut DB ausgegeben werden, um alle Probleme im Netz zu beheben, die sich über die vergangenen Jahre angesammelt haben. Der Zustand von Strecken und Gleisen wurde über viele Jahre vernachlässigt, sagt Bahnexperte Christian Böttger.
Die Bahn steht aktuell in keinem guten Licht. Zu viele Verspätungen, Zugausfälle und marode Infrastruktur.
Doch wie steht es wirklich um den Zustand der Bahn?
In dieser Analyse wird auf Daten der Deutschen Bahn zugegriffen, um dieser Frage auf den Grund zu gehen.
Die Bahn stellt über den API Marketplace eine Fülle an Daten zur Verfügung.
Im Rahmen dieses Projekts werden folgende Daten betrachtet, welche kostenlos zur Verfügung stehen. Für weitere Daten sind kostenpflichtige Pläne nötig.
Einige APIs liefern aber auch ähnliche Funktionalität nur in anderem Format oder noch etwas detailierter, z.B. RIS::Stations und StaDa. - Als Grundlage dienst RIS::Stations, darüber lassen sich alle deutschen Bahnhöfe abrufen. - FaSta - Station Facility Status gibt Auskunft über den Zustand der Bahnhöfe - Railway-Stations Pictures ermöglich den Zugriff auf Bilder jedes einzelnen Bahnhofs. - Facility Stations sind alle Dienstleistungen rund um die Bahnhöfe.
Daten abrufen
Über folgende APIs werden die Daten im JSON-Format abgerufen, in pandas dataframes umgewandelt und gespeichert.
url_parking = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2/'
url_ris_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/ris-stations/v1/'
url_rw_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/api.railway-stations.org/photoStationById/'
url_facility_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/fasta/v2/stations/'
Häufig können nicht alle Daten auf einmal abgerufen werden, daher müssen mehrere Aufrufe gemacht werden. Anschlißend ist ein mapping der JSON Bahn-API-Datenstruktur auf ein flaches Data Frame nötig.
Da der Abruf der Daten einige Minuten dauert, ist dieser und die eigentliche Auswertung getrennt in zwei verschiedenen files.
Imports
Es werden Dateien im Pickel-Format genutzt, um ganze Pandas Dataframes zwischen dem Scraping-Prozess und der Datenauswertung auszutauschen.
Der Vorteil dabei ist, dass die Struktur und die Datenformate dabei beibehalten werden.
Für die Visualisierung wird Plotly und Folium genutzt.
Da für gewisse Funktionen Python 3.10.1 benötigt wurde, bringt das Projekt eine eigene virtuelle Python Umgebung mit. Allerdings werden nur die folgenden Pakete benötigt
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import pickle
import plotly.express as px
import plotly.graph_objects as go
import folium
import folium.plugins as plugins
from geopy.geocoders import NominatimDaten laden
Hier werden die Daten aus den Pickel-Files in Dataframes geladen. Da diese Funktionalität häufig benötigt wird, ist sie in eine Funktion ausgelagert. Die Daten aus dem Scraping liegen im data Ordner.
data_folder = 'data/'def loadData(fileName):
with open(data_folder + fileName, 'rb') as pkl_file:
return pickle.load(pkl_file)df_stations = loadData('stations.pkl')
df_stopplaces = loadData('stopplaces_new.pkl')
df_facilities = loadData('station_facilities.pkl')Es kann zwar mehrere Bilder pro Bahnhof geben, dies wurde beim data scraping allerdings bereits berücksichtigt, sodass hier immer genau ein Bild pro Bahnhof vorhanden ist.
Die Bilder sind noch als Dictionary gespeichert, sodass dies hier noch in ein Pandas DataFrame umgewandelt werden muss.
df_station_images = loadData('station_images.pkl')
df_images = pd.DataFrame.from_dict({k: v for k, v in df_station_images.items() if v}).T
df_images.columns = ['image']
df_images = df_images.reset_index()Die verschiedenen Daten passen von der Anzahl nicht komplett zusammen.
Es ist aber durchaus erklärbar, dass es mehr Einrichtungen und Haltestellen als tatsächliche Bahnhöfe bzw. Bahnhofsgebäude gibt. Wie genau die Daten aussehen, wird im Folgenden geprüft.
print(f'Stations: {df_stations.shape}')
print(f'Station images: {df_images.shape}')
print(f'Facilities: {df_facilities.shape}')
print(f'Stopplaces: {df_stopplaces.shape}')Stations: (5690, 16)
Station images: (5627, 2)
Facilities: (3550, 6)
Stopplaces: (5727, 9)
Bahnhöfe
Das Data Frame der Bahnhöfe enthält alle Haltestellen der Deutschen Bahn in Deutschland.
Jeder Bahnhof hat eine eindeutige id. Zusätzlich wird beispielsweise der Name, die Adresse und Geo-Koordinaten mitgeliefert.
df_stations.head(3)| id | name | metropolis | street | houseNumber | postalCode | city | state | country | stationCategory | owner | organisationalUnit | countryCode | latitude | longitude | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | {} | Bahnhofstr. | 2a | 52064 | Aachen | Nordrhein-Westfalen | DE | CATEGORY_2 | DB S&S | RB West | DE | 50.767800 | 6.091499 | Europe/Berlin |
| 1 | 1000 | Burkhardswalde-Maxen | {} | Gesundbrunnen | 60c | 01809 | Müglitztal-Burkhardswalde | Sachsen | DE | CATEGORY_7 | DB S&S | RB Südost | DE | 50.925146 | 13.838369 | Europe/Berlin |
| 2 | 1001 | Burkhardtsdorf | {} | Bahnhofstraße | NaN | 09235 | Burkhardtsdorf | Sachsen | DE | CATEGORY_6 | DB Regio-Netze | Erzgebirgsbahn (EGB) | DE | NaN | NaN | Europe/Berlin |
Bilder von Bahnhöfen
Nutzer können Bilder zu Bahnhöfen hochladen. Das Data Frame enthält Links zu diesen Bildern. Die Spalte index referenziert die Spalte id der Bahnhöfe.
df_images.head(3)| index | image | |
|---|---|---|
| 0 | 1 | https://api.railway-stations.org/photos/de/1_1... |
| 1 | 1000 | https://api.railway-stations.org/photos/de/100... |
| 2 | 1001 | https://api.railway-stations.org/photos/de/100... |
Hier ein Beispiel vom Bahnhof in Aachen.
Einrichtungen
Die Einrichtungen sind beispielsweise Geräte wie Aufzüge auf Bahnhöfen. Das interessante hier ist, dass auch der Zustand mitgegeben wird, ob die Einrichtung funktionstüchtig ist.
df_facilities.head(3)| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1 | 1 | zu Gleis 2/3 | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2 | 1 | zu Gleis 6/7 | DB Station&Service | ACTIVE | available | ELEVATOR |
In Wahrheit gibt es hier genau zwei verschiedene Arten von Einrichtungen: Aufzüge und Rolltreppen.
df_facilities['type'].unique()array(['ELEVATOR', 'ESCALATOR'], dtype=object)
Haltestellen
Haltestellen enthalten viele Informationen der Bahnhöfe, zusätzlich aber auch die Transportmittel und welcher Verkehrsbund gilt.
df_stopplaces.head(3)| id | name | availableTransports | transportAssociations | countryCode | state | timeZone | latitude | longitude | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | DE | NW | Europe/Berlin | 50.767800 | 6.091499 |
| 1 | 1000 | Burkhardswalde-Maxen | [REGIONAL_TRAIN] | [VVO] | DE | SN | Europe/Berlin | 50.925146 | 13.838369 |
| 2 | 1001 | Burkhardtsdorf | [REGIONAL_TRAIN, BUS] | [VMS] | DE | SN | Europe/Berlin | 50.733196 | 12.932137 |
Explorative Datenanalyse
Datentypen
Als erstest wird geprüft, ob die Data Frames korrekte Datentypen haben und werden entsprechend korrigiert.
Die Spalte id soll immer vom Typ int sein, um sie später besser zusammenführen zu können.
df_stations['id'] = df_stations['id'].astype(int)
df_stations.dtypesid int32
name object
metropolis object
street object
houseNumber object
postalCode object
city object
state object
country object
stationCategory object
owner object
organisationalUnit object
countryCode object
latitude float64
longitude float64
timeZone object
dtype: object
df_stopplaces['id'] = df_stopplaces['id'].astype(int)
df_stopplaces.dtypesid int32
name object
availableTransports object
transportAssociations object
countryCode object
state object
timeZone object
latitude float64
longitude float64
dtype: object
df_facilities['id'] = df_facilities['id'].astype(int)
df_facilities.dtypesid int32
description object
operatorname object
state object
stateExplanation object
type object
dtype: object
df_images['index'] = df_images['index'].astype(int)
df_images.dtypesindex int32
image object
dtype: object
Fehlende Werte
Als nächsten wird auf fehlende Werte geprüft, um zu schauen, ob hier etwas zu korrigieren ist.
df_stations.isna().sum()id 0
name 0
metropolis 0
street 8
houseNumber 893
postalCode 7
city 4
state 0
country 0
stationCategory 12
owner 0
organisationalUnit 0
countryCode 0
latitude 282
longitude 282
timeZone 0
dtype: int64
Das Hausnummernfeld fehlt sehr oft, da diese Information aber nicht genutzt wird, ist das kein Problem.
Die Angaben für Latitude und Longitude fehlen häufig, hier kann über die Adresse versucht werden, die fehlenden Werte herauszufinden.
Da das Nachschauen der Werte einige Zeit in Anspruch nimmt, ist dieser Code in der finalen Abgabe auskommentiert.
Grundsätzlich wird aber anhand der Spalten postalCode, city, state und country mithilfe des Pakets aus der Vorlesung geopy versucht, die Geo-Koordinaten aufzulösen.
# geolocator = Nominatim(user_agent="my_app")
# filtered_rows = df_stations[df_stations['latitude'].isnull()]
# result = {}
# # Print the entire row for each entry with NaN latitude
# for index, row in filtered_rows.iterrows():
# try:
# address = f'{row["postalCode"]} {row["city"]} {row["state"]} {row["country"]}'
# result[row['id']] = geolocator.geocode(address)
# except:
# pass# dict={}
# # extract lat/lon
# for index, entry in result.items():
# if entry:
# dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }
# geocode_result = pd.DataFrame().from_dict(dict).T
# geocode_result['id'] = geocode_result.index
# geocode_result['id'] = geocode_result['id'].astype(int)Die Ergebnisse werden in einem Pickel-File gespeichert und daraus wieder geladen.
# output = open(data_folder + 'manual_geocode_results.pkl', 'wb')
# pickle.dump(geocode_result, output)
# output.close()geocode_result = loadData('manual_geocode_results.pkl')df_stations| id | name | metropolis | street | houseNumber | postalCode | city | state | country | stationCategory | owner | organisationalUnit | countryCode | latitude | longitude | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | {} | Bahnhofstr. | 2a | 52064 | Aachen | Nordrhein-Westfalen | DE | CATEGORY_2 | DB S&S | RB West | DE | 50.767800 | 6.091499 | Europe/Berlin |
| 1 | 1000 | Burkhardswalde-Maxen | {} | Gesundbrunnen | 60c | 01809 | Müglitztal-Burkhardswalde | Sachsen | DE | CATEGORY_7 | DB S&S | RB Südost | DE | 50.925146 | 13.838369 | Europe/Berlin |
| 2 | 1001 | Burkhardtsdorf | {} | Bahnhofstraße | NaN | 09235 | Burkhardtsdorf | Sachsen | DE | CATEGORY_6 | DB Regio-Netze | Erzgebirgsbahn (EGB) | DE | NaN | NaN | Europe/Berlin |
| 3 | 1002 | Bürstadt | {} | Bahnhofsallee | 17 | 68642 | Bürstadt | Hessen | DE | CATEGORY_6 | DB S&S | RB Mitte | DE | 49.645769 | 8.458188 | Europe/Berlin |
| 4 | 1005 | Buschow | {} | Bahnhofstr. | 28 | 14715 | Märkisch Luch OT Buschow | Brandenburg | DE | CATEGORY_6 | DB S&S | RB Ost | DE | 52.592203 | 12.628996 | Europe/Berlin |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5685 | 995 | Burgstädt | {} | Bahnhofstr. | 1 | 09217 | Burgstädt | Sachsen | DE | CATEGORY_6 | DB S&S | RB Südost | DE | 50.915817 | 12.812707 | Europe/Berlin |
| 5686 | 996 | Burgstall (Murr) | {} | Bahnhofstr. | 1 | 71576 | Burgstetten | Baden-Württemberg | DE | CATEGORY_6 | DB S&S | RB Südwest | DE | 48.928647 | 9.369932 | Europe/Berlin |
| 5687 | 997 | Steinfurt-Burgsteinfurt | {} | Bahnhofsplatz | 6 | 48565 | Steinfurt-Burgsteinfurt | Nordrhein-Westfalen | DE | CATEGORY_6 | DB S&S | RB West | DE | 52.147384 | 7.329340 | Europe/Berlin |
| 5688 | 998 | Burgthann | {} | Bahnhofstr. | 40 | 90559 | Burgthann | Bayern | DE | CATEGORY_5 | DB S&S | RB Süd | DE | 49.342474 | 11.309307 | Europe/Berlin |
| 5689 | 999 | Regensburg-Burgweinting | {} | Alfons-Goppel-Straße | NaN | 93055 | Regensburg | Bayern | DE | CATEGORY_6 | DB S&S | RB Süd | DE | 48.990725 | 12.146486 | Europe/Berlin |
5690 rows × 16 columns
Nun liegen die vorhanden Geo-Koordinaten im Data Frame df_stations und die neu ermittelten in geocode_result.
Um nun das Ergebnis aus beiden Tabellen zu bekommen, wird die Funktion combine_first genutzt.
df_stations = df_stations.set_index('id').combine_first(geocode_result.set_index('id')).reset_index()Anstelle von 282 fehlenden Werten sind es jetzt nur noch 24!
Die restlichen werden aufgrund der geringen Anzahl ignoriert.
df_stations.isna().sum()id 0
city 4
country 0
countryCode 0
houseNumber 893
latitude 24
longitude 24
metropolis 0
name 0
organisationalUnit 0
owner 0
postalCode 7
state 0
stationCategory 12
street 8
timeZone 0
dtype: int64
df_stopplaces.isna().sum()id 0
name 0
availableTransports 0
transportAssociations 0
countryCode 0
state 11
timeZone 0
latitude 0
longitude 0
dtype: int64
Die Haltestellen scheinen eine bessere Datenqualität zu haben, hier gibt es keine Probleme, die betrachtet werden müssen.
df_facilities.isna().sum()id 0
description 51
operatorname 0
state 0
stateExplanation 0
type 0
dtype: int64
Einige Beschreibungen sind leer. Diese beschreiben meist, wohin der Aufzug oder die Rolltreppe führen, z.B. “zu Gleis 1”. Aus dieser Information lässt sich nichts ableiten, somit sind die leeren Felder zu vernachlässigen.
Das gute ist, dass werder type noch state jemals leer ist.
df_facilities.head(1)| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
df_facilities[df_facilities['description'].isna()]| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 4 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 5 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 6 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 7 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 274 | 1390 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 375 | 161 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 630 | 1866 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 648 | 1866 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 660 | 1867 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 661 | 1867 | None | DB Station&Service | UNKNOWN | monitoring disrupted | ELEVATOR |
| 662 | 1867 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 676 | 1877 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 687 | 1907 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 688 | 1907 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 794 | 220 | None | DB Station&Service | INACTIVE | under construction | ESCALATOR |
| 861 | 2420 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 862 | 2420 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1010 | 2545 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1024 | 2551 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1042 | 2618 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1043 | 2618 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1133 | 2827 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1462 | 3631 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1752 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1755 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1756 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1761 | 4234 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1775 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1776 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1783 | 4236 | None | DB Station&Service | UNKNOWN | monitoring not available | ELEVATOR |
| 1828 | 4242 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1848 | 4258 | None | DB Station&Service | UNKNOWN | monitoring disrupted | ESCALATOR |
| 2007 | 4587 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2026 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2027 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2028 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2035 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2036 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2037 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2041 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2176 | 4809 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 2177 | 4809 | None | DB Station&Service | INACTIVE | not available | ESCALATOR |
| 2178 | 4809 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 2179 | 4809 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 2763 | 5844 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2764 | 5844 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2840 | 6071 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 3064 | 6612 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 3065 | 6612 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 3408 | 8097 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 3409 | 8097 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
Daten zusammen bringen
Jetzt können alle Daten pro Bahnhofsstation zusammengebracht werden.
Dazu werden zunächst alle doppelten Spalten entfernt und dann die Tabellen mithilfe zweier merge zusammengefügt.
Davor wird nochmal stichprobenartig geprüft, ob die IDs auch wirklich zusammenpassen.
Die Facilities können dabei nicht gejoined werden, da ein Bahnhof in der Regel mehrere davon aufweist (1:n Beziehung)
df_stopplaces[df_stopplaces['name'] == 'Ahrensfelde']| id | name | availableTransports | transportAssociations | countryCode | state | timeZone | latitude | longitude | |
|---|---|---|---|---|---|---|---|---|---|
| 1539 | 28 | Ahrensfelde | [REGIONAL_TRAIN] | [VBB] | DE | BE | Europe/Berlin | 52.571375 | 13.565154 |
df_stations[df_stations['name'] == 'Ahrensfelde']| id | city | country | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23 | 28 | Berlin | DE | DE | NaN | 52.571375 | 13.565154 | {} | Ahrensfelde | RB Ost | DB S&S | 12689 | Berlin | CATEGORY_4 | Märkische Allee | Europe/Berlin |
df_stopplaces.drop(columns=['name', 'state', 'countryCode', 'latitude', 'longitude','timeZone'], inplace=True)
df = pd.merge(df_stations, df_stopplaces, on='id', how='left')
df = pd.merge(left=df, right=df_images, left_on=['id'], right_on=['index'], how='left')
df.drop(columns=['timeZone','index','country'], inplace=True)df.isna().sum()id 0
city 4
countryCode 0
houseNumber 903
latitude 24
longitude 24
metropolis 0
name 0
organisationalUnit 0
owner 0
postalCode 7
state 0
stationCategory 12
street 8
availableTransports 18
transportAssociations 18
image 64
dtype: int64
Es fehlen nun noch einzelne Werte, aber mit dieser Datengrundlage kann gut gearbeitet werden.
Datenvisualisierung
Anhand der Daten ergeben sich unterschiedliche Fragen, wie beispielsweise:
- Sind tatsächlich nur deutsche Stationen vorhanden?
- Wie sind die Betreiber
ownerund VerkehrsverbündeorganisationalUnitausgeprägt?
- Wie sind die Betreiber
- Wie verteilen sich die Stationen innerhalb von Deutschland (Geo-Koordinaten anschauen)
- Wie sind die Transportmittel
transportAssociationsausgeprägt?
- Wie sind die Transportmittel
- Wofür stehen die Werte von
stationCategory?
- Wofür stehen die Werte von
df.head(1)| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.7678 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... |
a) Bahnhöfe der Schweiz
Es gibt im Datenbestand auch einige Bahnhöfe, die in der Schweiz liegen.
Beispielsweise Schaffhausen ist ein Gemeinschaftsbahnhof zwischen der Schweizerischen Bundesbahnen und dem deutschen Bundeseisenbahnvermögen. Quelle: Wikipedia
Auffällig ist, dass bei diesen Bahnhöfen keine Bilder und auch keine stationCategory / transportAssociations vorhanden sind, das scheint nur im Datenbestand der deutschen Bahnhöfe zu exisiteren.
df[df['countryCode']!='DE']| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 347 | 424 | Basel | CH | 200 | 47.567288 | 7.607805 | {} | Basel Bad Bf | RB Südwest | DB S&S | 4016 | Schweiz CH | NaN | Schwarzwaldallee | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [RVL] | NaN |
| 2093 | 2698 | Schaffhausen | CH | 1 | 47.717003 | 8.664127 | {} | Herblingen | RB Südwest | DB S&S | 8207 | Schweiz CH | NaN | Bruderhalde | [CITY_TRAIN] | [] | NaN |
| 3368 | 4399 | Neuhausen | CH | 18 | 47.682615 | 8.612186 | {} | Neuhausen Bad Bf | RB Südwest | DB S&S | 8212 | Schweiz CH | NaN | Badischen Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 3387 | 4424 | Neunkirch | CH | 3 | 47.689151 | 8.495384 | {} | Neunkirch | RB Südwest | DB S&S | 8225 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 4053 | 5274 | Riehen | CH | 25 | 47.583157 | 7.652014 | {} | Riehen | RB Südwest | DB S&S | 4125 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [RVL] | NaN |
| 4233 | 5530 | Schaffhausen | CH | 29 | NaN | NaN | {} | Schaffhausen | RB Südwest | DB S&S | 8200 | Schweiz CH | NaN | Bahnhofstr. | NaN | NaN | NaN |
| 4715 | 6192 | Thayngen | CH | 31 | 47.745502 | 8.704300 | {} | Thayngen | RB Südwest | DB S&S | 8240 | Schweiz CH | NaN | Bahnhofstr. | [CITY_TRAIN] | [] | NaN |
| 4743 | 6235 | Trasadingen | CH | 1 | 47.665238 | 8.436804 | {} | Trasadingen | RB Südwest | DB S&S | 8219 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 5128 | 6762 | Wilchingen | CH | 18 | 47.679448 | 8.463860 | {} | Wilchingen-Hallau | RB Südwest | DB S&S | 8217 | Schweiz CH | NaN | Bahnhofstrasse | [REGIONAL_TRAIN] | [] | NaN |
b) Betreiber und Verkehrsbünde
def plot_counts(column):
counts = df[column].value_counts().reset_index()
counts.columns = [column, 'count']
fig = px.bar(counts, x=column, y='count', barmode='group', text='count')
fig.show()Die meisten Bahnhöfe gehören der DB Station&Service AG. Laut ihrer Webseite, unterhalten sie rund 5.400 Bahnhöfe.
Das können wir bestätigen! The DB S&S hat laut den Daten 5.413 Bahnhöfe. Der Rest, 277, werden von der DB Regio-Netze unterhalten.
plot_counts('owner')Unable to display output for mime type(s): application/vnd.plotly.v1+json
Betrachtet man die Organisationsbereiche, gibt es viele Stationen in der Mitte/ im Süden/ im Westen von Deutschland. Der Norden und Osten liegen hingegen auf den letzten Plätzen.
Es gibt auch einige kleinere Organisationseinheiten für spezielle Regionen.
plot_counts('organisationalUnit')Unable to display output for mime type(s): application/vnd.plotly.v1+json
Tatsächlich ist zu erkennen, dass die führenden Bundesländer Bayern, Baden-Württemberg und Nordrhein-Westfalen (NRW) sind.
Es besteht eine deutliche Lücke zwischen ihnen und dem viertplatzierten Bundesland Hessen. Natürlich muss bei der Anzahl der Bahnhöfe aber auch die Größe der Bundesländer berücksichtigt werden.
Daher wird geprüft, welches Bundesland laut seiner Größe die meisten Bahnhöfe hat. Dazu kann die Größe der Bundesländer abgerufen und ins Verhältnis mit der Anzahl der Stationen gesetzt werden.
c) Bahnhofsdichte in Deutschland
Größe der Bundesländer:
df_states = pd.read_csv(data_folder + 'states_size.csv', sep=';')
df_states['size'] = df_states['size'].astype(float)
df_states| state | size | |
|---|---|---|
| 0 | Baden-Württemberg | 35747.82 |
| 1 | Bayern | 70541.57 |
| 2 | Berlin | 891.12 |
| 3 | Brandenburg | 29654.35 |
| 4 | Bremen | 419.62 |
| 5 | Hamburg | 755.09 |
| 6 | Hessen | 21115.64 |
| 7 | Mecklenburg-Vorpommern | 23295.45 |
| 8 | Niedersachsen | 47709.82 |
| 9 | Nordrhein-Westfalen | 34112.44 |
| 10 | Rheinland-Pfalz | 19858.00 |
| 11 | Saarland | 2571.11 |
| 12 | Sachsen | 18449.93 |
| 13 | Sachsen-Anhalt | 20459.12 |
| 14 | Schleswig-Holstein | 15804.30 |
| 15 | Thüringen | 16202.39 |
| 16 | Deutschland | 357587.77 |
Anzahl Bahnhöfe pro Bundesland:
df_grp_states = pd.DataFrame(df_stations.groupby(by='state').count()['id'].sort_values(ascending=False))
df_grp_states.rename(columns={'id': 'count'}, inplace=True)
df_grp_states| count | |
|---|---|
| state | |
| Bayern | 1025 |
| Baden-Württemberg | 720 |
| Nordrhein-Westfalen | 711 |
| Hessen | 479 |
| Sachsen | 478 |
| Rheinland-Pfalz | 419 |
| Niedersachsen | 357 |
| Brandenburg | 310 |
| Sachsen-Anhalt | 289 |
| Thüringen | 289 |
| Mecklenburg-Vorpommern | 180 |
| Schleswig-Holstein | 137 |
| Berlin | 133 |
| Saarland | 77 |
| Hamburg | 58 |
| Bremen | 16 |
| Schweiz CH | 12 |
df_germany = pd.DataFrame(index=['count'], data={
'Deutschland':df_grp_states.sum().values[0]
}).T
df_grp_states = pd.concat([df_grp_states, df_germany])
df_grp_states['state'] = df_grp_states.index
df_grp_states = pd.merge(df_states, df_grp_states, how='left', on='state')df_grp_states| state | size | count | |
|---|---|---|---|
| 0 | Baden-Württemberg | 35747.82 | 720 |
| 1 | Bayern | 70541.57 | 1025 |
| 2 | Berlin | 891.12 | 133 |
| 3 | Brandenburg | 29654.35 | 310 |
| 4 | Bremen | 419.62 | 16 |
| 5 | Hamburg | 755.09 | 58 |
| 6 | Hessen | 21115.64 | 479 |
| 7 | Mecklenburg-Vorpommern | 23295.45 | 180 |
| 8 | Niedersachsen | 47709.82 | 357 |
| 9 | Nordrhein-Westfalen | 34112.44 | 711 |
| 10 | Rheinland-Pfalz | 19858.00 | 419 |
| 11 | Saarland | 2571.11 | 77 |
| 12 | Sachsen | 18449.93 | 478 |
| 13 | Sachsen-Anhalt | 20459.12 | 289 |
| 14 | Schleswig-Holstein | 15804.30 | 137 |
| 15 | Thüringen | 16202.39 | 289 |
| 16 | Deutschland | 357587.77 | 5690 |
- Gemäß seiner Größe hat Berlin die meisten Haltestellen, wobei jede Station durchschnittlich etwa 6,7 km² abdeckt. Für eine Hauptstadt ergibt dies Sinn.
- Im Allgemeinen haben alle großen Städte (Hamburg, Bremen) dieses gute Verhältnis.
- Zuvor waren Bayern und Baden-Württemberg die Regionen mit den meisten Stationen, nun befinden sie sich auf den Plätzen 11 und 8.
- Das Saarland, das die wenigsten Bahnhöfe hat, weist das beste Verhältnis der Fläche zu Stationen unter den Bundesländern auf.
- In Niedersachsen muss eine Station im Durchschnitt etwa 133,6 km² abdecken, das ist viel, wenn man so weit mit dem Auto fahren muss, um zur nächstgelegenen Bahn-Station zu gelangen.
- Der Durchschnitt für ganz Deutschland liegt bei 62 km².
df_grp_states['ratio'] = df_grp_states['size']/df_grp_states['count']
df_grp_states.sort_values('ratio', ascending=True).reset_index().drop(columns=['index'])| state | size | count | ratio | |
|---|---|---|---|---|
| 0 | Berlin | 891.12 | 133 | 6.700150 |
| 1 | Hamburg | 755.09 | 58 | 13.018793 |
| 2 | Bremen | 419.62 | 16 | 26.226250 |
| 3 | Saarland | 2571.11 | 77 | 33.391039 |
| 4 | Sachsen | 18449.93 | 478 | 38.598180 |
| 5 | Hessen | 21115.64 | 479 | 44.082756 |
| 6 | Rheinland-Pfalz | 19858.00 | 419 | 47.393795 |
| 7 | Nordrhein-Westfalen | 34112.44 | 711 | 47.978115 |
| 8 | Baden-Württemberg | 35747.82 | 720 | 49.649750 |
| 9 | Thüringen | 16202.39 | 289 | 56.063633 |
| 10 | Deutschland | 357587.77 | 5690 | 62.844951 |
| 11 | Bayern | 70541.57 | 1025 | 68.821044 |
| 12 | Sachsen-Anhalt | 20459.12 | 289 | 70.792803 |
| 13 | Brandenburg | 29654.35 | 310 | 95.659194 |
| 14 | Schleswig-Holstein | 15804.30 | 137 | 115.359854 |
| 15 | Mecklenburg-Vorpommern | 23295.45 | 180 | 129.419167 |
| 16 | Niedersachsen | 47709.82 | 357 | 133.640952 |
Um das ganze auf einer Karte anzuzeigen, fehlen noch die Geo-Koordinaten der Bundesländer. Diese werden wie zuvor abgerufen.
geolocator = Nominatim(user_agent="my_app")
result={}
for entry in df_grp_states['state']:
result[entry] = geolocator.geocode(entry)res_dict={}
# extract lat/lon
for index, entry in result.items():
if entry:
res_dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }
geocode_result = pd.DataFrame().from_dict(res_dict).T
geocode_result['state'] = geocode_result.index
geocode_result| latitude | longitude | state | |
|---|---|---|---|
| Baden-Württemberg | 48.537750 | 9.041169 | Baden-Württemberg |
| Bayern | 48.946756 | 11.403872 | Bayern |
| Berlin | 52.517037 | 13.388860 | Berlin |
| Brandenburg | 52.845549 | 13.246130 | Brandenburg |
| Bremen | 53.075820 | 8.807165 | Bremen |
| Hamburg | 53.550341 | 10.000654 | Hamburg |
| Hessen | 50.608065 | 9.028465 | Hessen |
| Mecklenburg-Vorpommern | 53.773506 | 12.575547 | Mecklenburg-Vorpommern |
| Niedersachsen | 52.839853 | 9.075962 | Niedersachsen |
| Nordrhein-Westfalen | 51.478921 | 7.554375 | Nordrhein-Westfalen |
| Rheinland-Pfalz | 49.953160 | 7.310646 | Rheinland-Pfalz |
| Saarland | 49.384187 | 6.953737 | Saarland |
| Sachsen | 50.929580 | 13.458505 | Sachsen |
| Sachsen-Anhalt | 52.008907 | 11.700334 | Sachsen-Anhalt |
| Schleswig-Holstein | 54.185400 | 9.822009 | Schleswig-Holstein |
| Thüringen | 50.901472 | 11.037784 | Thüringen |
| Deutschland | 51.163818 | 10.447831 | Deutschland |
df_grp_states_geo = pd.merge(df_grp_states, geocode_result, how='left', on='state')
df_grp_states_geo.drop(16, inplace=True) # drop germanyIn diesem Fall wird plotly genutzt, um eine Karte anzuzeigen.
Die Größe der Punkte zeigt die Anzahl der Haltestationen an.
Die Farbe gibt das Verhältnis zur Fläche an. Grün steht für eine hohe Dichte an Stationen, rot für eine niedrige.
Hier wird nochmal deutlich, dass die Stadtstaaten eine hohe Dichte an Haltestationen aufweisen, allerdings absolut gesehen wenige Stationen haben (kleiner Kreis).
Die großen Bundesländer sind eher im mittleren Farbschema, wobei die im Norden rot gefärbt und damit Schlusslicht sind.
fig = px.scatter_geo(df_grp_states_geo,
lat='latitude',
lon='longitude',
hover_name='state', # Data to display when hovering over each data point
size='count', # Size of the markers
color='ratio', # Color of the markers
color_continuous_scale=['green','orange','red'],
projection='mercator',
scope='europe',
width=650,
height=800) # Map projection
fig.update_geos(center=dict(lon=10, lat=51), projection_scale=10)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
d) Verkehrsmittel und Verkehrsverbünde
Da die Verkehrsmittel und Verkehrsverbünde in geschachtelten Listen vorliegen, müssen diese zunächst geebnet werden, um die absolute Anzahl herauszufinden.
transports = []
for entry in df['transportAssociations']:
try:
for e in entry:
transports.append(e)
except:
pass
transportAssociations = pd.Series(transports).value_counts()transports = []
for entry in df['availableTransports']:
try:
for e in entry:
transports.append(e)
except:
pass
availableTransports = pd.Series(transports).value_counts()def plotBar(data, title, xlabel, ylabel, showlegend):
fig = px.bar(data, title=title)
fig.update_xaxes(title_text=xlabel)
fig.update_yaxes(title_text=ylabel)
fig.update_traces(showlegend=showlegend)
return figfig = plotBar(transportAssociations, 'Available Transport Associations', 'Transport Associations', 'Count', False)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
transportAssociations.count()49
Spitzenreiter ist auch hier wieder Berlin, gefolgt von dem Rhein-Main-Verkehrsverbund (RMV).
Der RMV operiert in Hessen und ist der Nachfolger des Frankfurter Verkehrsverbundes (FVV) (Quelle: Wikipedia) Hier sieht man das Verkehrsgebiet in Hessen.
Die “NASA” ist der Nahverkehrsservice der Sachsen-Anhalt GmbH und hat lustigerweise auch die Webseite https://www.nasa.de/.
Man sieht auf dem Plan, dass hauptsächlich die Städt Leipzig, Halle, Dessau und Magdeburg verbunden werden und der “NASA” aus anderen Verkehrsverbünden bestehen, die ebenfalls hier auftauchen, wie der MDV und Übergänge zu anderen Verkehrsverbünden hat wie VMT, VRB und VBB.
Die in unserer Region bekannteren Verkehrsverbünde VVS und NALDO rangieren auf den mittleren Plätzen. Insgesamt gibt es 49 unterschiedliche Verkehrsverbünde.
fig = plotBar(availableTransports, 'Available Transports', 'Transport Type', 'Count', False)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Mit großem Abstand gibt es Haltestationen, an denen die REGIONAL_TRAIN hält. Leider ist nicht dokumentiert, was darunter zu verstehen ist.
Nach manueller Untersuchung der Daten werden damit sowohl Regionalbahnen (RB) wie auch S-Bahnen gemeint sein. Allerdings werden die S-Bahn Stationen in der Stadt mit CITY_TRAIN markiert. Das ist also nicht ganz eindeutig.
Neben Zügen werden hier auch Busse (BUS) erfasst sowie CITY_TRAIN, was die Stadtbahnen/Tram/U-Bahn sowie S-Bahnen in der Stadt sind.
Im Vergleich dazu kommen die Schnellzüge INTERCITY_TRAIN (IC), HIGH_SPEED_TRAIN (ICE) und INTER_REGIONAL_TRAIN (IRE) fast schon selten vor. Das macht aber natürlich Sinn, weil diese nur an ausgewählten Bahnhöfen halten.
Hier gibt es eine Übersicht der Bahn über die Nah- und Fernverkehrszüge.
e) Bahnhofskategorien
Laut Theorie klassifiziert die Preisklasse (bis 2018 Bahnhofskategorie) anhand verschiedener Faktoren die Bedeutung eines Bahnhofs für den Personenverkehr sowie den Service, der dort geboten wird. (Quelle: Wikipedia)
Demnach wird die Preisklasse aufgrund folgender Kriterien ermittelt:
Daraus lässt sich ein Wert errechnen, der die Preisklasse angibt:
Beispiel für einen Bahnhof mit 4 Bahnsteigkanten von max. 250 Meter Länge, 20.000 Reisenden und 280 Zughalten am Tag, ohne anwesenden Mitarbeiter, jedoch mit technischer Stufenfreiheit:
Der Bahnhof in dem Beispiel hätte also Klasse 3.
Laut Wikipedia ergibt sich für Deutschland folgende Einteilung. Mal sehen, ob sich das mit den vorhandenen Daten deckt:
| Kategorie | Anzahl | Beispiel |
|---|---|---|
| 1 | 21 | Berlin, Hamburg |
| 2 | 87 | Mainz, Trier |
| 3 | 256 | Hof, Bitterfel |
| 4 | 628 | Meiningen, Bingen |
| 5 | 992 | Köln-Holweide, Hohen Neuendorf |
| 6 | 2505 | Ilmenau, Glöwen |
| 7 | 916 | Zwotental, Göhrde |
Aufgrund des merge kommen nun ein paar Bahnhöfe doppelt vor, in denen es zwei Haltestationen gibt. Meist sind das größere Bahnhöfe, in denen die Schnellzüge getrennt vom Regionalverkehr abfahren, wie hier in Berlin.
Um das zu korrigieren, wird jeweils nur der letzte Eintrag bei diesen doppelten Einträgen genommen.
df[df['id'] == 1071]| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 854 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [CITY_TRAIN] | [VBB] | https://api.railway-stations.org/photos/de/107... |
| 855 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/107... |
| 856 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/107... |
unique_df = df.drop_duplicates(subset='id', keep='last')Betrachtet man nun die Daten von Deutschland, ergibt sich folgendes Bild, in etwa passt die Einteilung also:
unique_df.groupby(by='stationCategory')['id'].count()stationCategory
CATEGORY_1 23
CATEGORY_2 84
CATEGORY_3 275
CATEGORY_4 641
CATEGORY_5 977
CATEGORY_6 2774
CATEGORY_7 904
Name: id, dtype: int64
In Berlin gibt es gleich mehrere Bahnhöfe mit Preisklasse / stationCategory 1. In Hamburg ist es der Hauptbahnhof und Altona. Stuttgart Hbf gehört auch zur Kategorie 1.
unique_df[unique_df['stationCategory'] == 'CATEGORY_1']| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 183 | 220 | Augsburg | DE | 1 | 48.365441 | 10.885570 | {} | Augsburg Hbf | RB Süd | DB S&S | 86150 | Bayern | CATEGORY_1 | Viktoriastr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AVV] | https://api.railway-stations.org/photos/de/220... |
| 421 | 528 | Berlin | DE | 1-3 | 52.548963 | 13.388513 | {} | Berlin Gesundbrunnen | RB Ost | DB S&S | 13357 | Berlin | CATEGORY_1 | Badstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VBB] | https://api.railway-stations.org/photos/de/528... |
| 424 | 530 | Berlin | DE | 3 | 52.510488 | 13.434681 | {} | Berlin Ostbahnhof | RB Ost | DB S&S | 10243 | Berlin | CATEGORY_1 | Koppenstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/530... |
| 856 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/107... |
| 1002 | 1289 | Dortmund | DE | 15 | 51.517896 | 7.459290 | {} | Dortmund Hbf | RB West | DB S&S | 44137 | Nordrhein-Westfalen | CATEGORY_1 | Königswall | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRR, WT] | https://api.railway-stations.org/photos/de/128... |
| 1052 | 1343 | Dresden | DE | 4 | 51.040563 | 13.732035 | {} | Dresden Hbf | RB Südost | DB S&S | 01069 | Sachsen | CATEGORY_1 | Wiener Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VVO] | https://api.railway-stations.org/photos/de/134... |
| 1075 | 1374 | Duisburg | DE | 1 | 51.429785 | 6.775903 | {} | Duisburg Hbf | RB West | DB S&S | 47051 | Nordrhein-Westfalen | CATEGORY_1 | Portsmouthplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRR] | https://api.railway-stations.org/photos/de/137... |
| 1097 | 1401 | Düsseldorf | DE | 14 | 51.219962 | 6.794319 | {} | Düsseldorf Hbf | RB West | DB S&S | 40210 | Nordrhein-Westfalen | CATEGORY_1 | Konrad-Adenauer-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRS, VRR] | https://api.railway-stations.org/photos/de/140... |
| 1329 | 1690 | Essen | DE | 5 | 51.451355 | 7.014793 | {} | Essen Hbf | RB West | DB S&S | 45127 | Nordrhein-Westfalen | CATEGORY_1 | Am Hauptbahnhof | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRR] | https://api.railway-stations.org/photos/de/169... |
| 1476 | 1866 | Frankfurt am Main | DE | NaN | 50.107145 | 8.663789 | {} | Frankfurt (Main) Hbf | RB Mitte | DB S&S | 60329 | Hessen | CATEGORY_1 | Im Hauptbahnhof | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [RMV] | https://api.railway-stations.org/photos/de/186... |
| 1947 | 2514 | Hamburg | DE | 16 | 53.552736 | 10.006909 | {} | Hamburg Hbf | RB Nord | DB S&S | 20099 | Hamburg | CATEGORY_1 | Hachmannplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [IAM, SH, HVV] | https://api.railway-stations.org/photos/de/251... |
| 1951 | 2517 | Hamburg | DE | 17 | 53.552695 | 9.935175 | {} | Hamburg-Altona | RB Nord | DB S&S | 22765 | Hamburg | CATEGORY_1 | Scheel-Plessen-Str. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [IAM, SH, HVV] | https://api.railway-stations.org/photos/de/251... |
| 1977 | 2545 | Hannover | DE | 1 | 52.376761 | 9.741021 | {} | Hannover Hbf | RB Nord | DB S&S | 30159 | Niedersachsen | CATEGORY_1 | Ernst-August-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [IAM, GVH] | https://api.railway-stations.org/photos/de/254... |
| 2405 | 3107 | Karlsruhe | DE | 1a | 48.993515 | 8.402181 | {} | Karlsruhe Hbf | RB Südwest | DB S&S | 76137 | Baden-Württemberg | CATEGORY_1 | Bahnhofplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [KVV] | https://api.railway-stations.org/photos/de/310... |
| 2554 | 3320 | Köln | DE | 11 | 50.943030 | 6.958729 | {} | Köln Hbf | RB West | DB S&S | 50667 | Nordrhein-Westfalen | CATEGORY_1 | Trankgasse | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRS] | https://api.railway-stations.org/photos/de/332... |
| 2564 | 3329 | Köln | DE | 7 | 50.940874 | 6.975001 | {} | Köln Messe/Deutz | RB West | DB S&S | 50679 | Nordrhein-Westfalen | CATEGORY_1 | Ottoplatz | [REGIONAL_TRAIN, BUS, HIGH_SPEED_TRAIN, CITY_T... | [VRS] | https://api.railway-stations.org/photos/de/332... |
| 2784 | 3631 | Leipzig | DE | 5 | 51.345471 | 12.382064 | {} | Leipzig Hbf | RB Südost | DB S&S | 04109 | Sachsen | CATEGORY_1 | Willy-Brandt-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [NASA, MDV] | https://api.railway-stations.org/photos/de/363... |
| 3003 | 3925 | Mannheim | DE | 17 | 49.479354 | 8.468921 | {} | Mannheim Hbf | RB Südwest | DB S&S | 68161 | Baden-Württemberg | CATEGORY_1 | Willy-Brandt-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRN] | https://api.railway-stations.org/photos/de/392... |
| 3236 | 4234 | München | DE | 10a | 48.140232 | 11.558335 | {} | München Hbf | RB Süd | DB S&S | 80335 | Bayern | CATEGORY_1 | Bayerstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [MVV] | https://api.railway-stations.org/photos/de/423... |
| 3243 | 4241 | München | DE | 11 | 48.127440 | 11.604971 | {} | München Ost | RB Süd | DB S&S | 81667 | Bayern | CATEGORY_1 | Orleansplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [MVV] | https://api.railway-stations.org/photos/de/424... |
| 3531 | 4593 | Nürnberg | DE | 9 | 49.445616 | 11.082989 | {} | Nürnberg Hbf | RB Süd | DB S&S | 90443 | Bayern | CATEGORY_1 | Bahnhofsplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VGN] | https://api.railway-stations.org/photos/de/459... |
| 3743 | 4859 | Berlin | DE | 1 | 52.475047 | 13.365319 | {} | Berlin Südkreuz | RB Ost | DB S&S | 12101 | Berlin | CATEGORY_1 | General-Pape-Straße | [] | [VBB] | https://api.railway-stations.org/photos/de/485... |
| 4622 | 6071 | Stuttgart | DE | 2 | 48.784084 | 9.181635 | {} | Stuttgart Hbf | RB Südwest | DB S&S | 70173 | Baden-Württemberg | CATEGORY_1 | Arnulf-Klett-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VVS] | https://api.railway-stations.org/photos/de/607... |
Betrachtet man die Bundesländer nach Kategorie, gibt es einige die keinen einzigen Bahnhof mit Preisklasse 1 haben.
df_state_categories = pd.DataFrame(unique_df.groupby(by=['state', 'stationCategory'])['id'].count())
df_state_categories = df_state_categories.reset_index().pivot(index='state', columns='stationCategory', values='id')
df_state_categories| stationCategory | CATEGORY_1 | CATEGORY_2 | CATEGORY_3 | CATEGORY_4 | CATEGORY_5 | CATEGORY_6 | CATEGORY_7 |
|---|---|---|---|---|---|---|---|
| state | |||||||
| Baden-Württemberg | 3.0 | 10.0 | 51.0 | 86.0 | 133.0 | 328.0 | 109.0 |
| Bayern | 4.0 | 9.0 | 48.0 | 100.0 | 174.0 | 532.0 | 158.0 |
| Berlin | 4.0 | 7.0 | 12.0 | 72.0 | 34.0 | 4.0 | NaN |
| Brandenburg | NaN | 4.0 | 13.0 | 22.0 | 44.0 | 174.0 | 53.0 |
| Bremen | NaN | 1.0 | 1.0 | 3.0 | 8.0 | 3.0 | NaN |
| Hamburg | 2.0 | 2.0 | 8.0 | 35.0 | 9.0 | 2.0 | NaN |
| Hessen | 1.0 | 8.0 | 24.0 | 62.0 | 109.0 | 248.0 | 27.0 |
| Mecklenburg-Vorpommern | NaN | 1.0 | 7.0 | 12.0 | 14.0 | 90.0 | 56.0 |
| Niedersachsen | 1.0 | 8.0 | 15.0 | 51.0 | 68.0 | 165.0 | 49.0 |
| Nordrhein-Westfalen | 6.0 | 16.0 | 42.0 | 101.0 | 166.0 | 307.0 | 73.0 |
| Rheinland-Pfalz | NaN | 7.0 | 12.0 | 37.0 | 67.0 | 222.0 | 74.0 |
| Saarland | NaN | 1.0 | 6.0 | 3.0 | 14.0 | 47.0 | 6.0 |
| Sachsen | 2.0 | 2.0 | 7.0 | 26.0 | 68.0 | 291.0 | 82.0 |
| Sachsen-Anhalt | NaN | 2.0 | 9.0 | 11.0 | 26.0 | 162.0 | 79.0 |
| Schleswig-Holstein | NaN | 4.0 | 10.0 | 10.0 | 28.0 | 49.0 | 36.0 |
| Thüringen | NaN | 2.0 | 10.0 | 10.0 | 15.0 | 150.0 | 102.0 |
Die größten Unterschiede gibt es in Preisklasse 6, hier liegen die Anzahl der Bahnhöfe je Bundesland weit außeinander. Bayern und Baden-Württemberg, die die meisten Bahnhöfe haben, sind hier Ausreißer.
In den restlichen Preisklassen ist das nicht so ausgeprägt, besonders NRW (gelb) ist in den oberen Kategorien zusammen mit Bayern und Baden-Württemberg häufig auf den besten Plätzen mit dabei.
fig = px.violin(df_state_categories, y=df_state_categories.columns,color=df_state_categories.index)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
In diesem Bild sieht man nochmal gut, dass die meisten Stationen in Preisklasse 6 sind. Bei den Stadtstaaten sind die Werte näher beieinander, dort ist die Anzahl der Stationen insgesamt natürlich geringer.
fig = px.scatter(df_state_categories, y=df_state_categories.columns)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Rolltreppen und Aufzüge
df_facilities.head(1)| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
Zunächst werden die Daten in eine Form gebracht, die besser zu visualisieren ist.
Von Interesse ist, welche Art (type) von facilities in welchem Zustand (state) ist.
Dazu wird anhand dieser beiden Werte gruppiert und die summierten Werte wieder in eine flache Struktur geformt.
Man könnte auch noch auf den operatorname eingehen, allerdings werden die allermeisten Einrichtungen wieder von der DB Station&Service betrieben.
df_facilities_grouped = df_facilities.groupby(['type', 'state']).count()
df_facilities_grouped = df_facilities_grouped.unstack()['id']
df_facilities_grouped| state | ACTIVE | INACTIVE | UNKNOWN |
|---|---|---|---|
| type | |||
| ELEVATOR | 2392 | 126 | 51 |
| ESCALATOR | 820 | 140 | 21 |
df_facilities_grouped['Ratio'] = (df_facilities_grouped['INACTIVE']) / df_facilities_grouped['ACTIVE']
df_facilities_grouped| state | ACTIVE | INACTIVE | UNKNOWN | Ratio |
|---|---|---|---|---|
| type | ||||
| ELEVATOR | 2392 | 126 | 51 | 0.052676 |
| ESCALATOR | 820 | 140 | 21 | 0.170732 |
Es gibt ein paar Daten, bei denen der Zustand unbekannt ist.
Darüber hinaus sind absolut und relativ gesehen aktuell mehr Rolltreppen kaputt als Aufzüge.
Relativ sind es mit über 15% (nach update eine Woche später: 17%) aktuell nicht funktionierende Rolltreppen wirklich viele.
fig = go.Figure()
for state in df_facilities_grouped.columns[:3]:
fig.add_trace(go.Bar(
x=df_facilities_grouped.index,
y=df_facilities_grouped[state],
name=state,
))
fig.update_layout(title='Zustand der Einrichtungen an Bahnhöfen',
xaxis_title='Typ',
yaxis_title='Anzahl',
barmode='group')
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Dashboard für Haltestationen
Wir können uns eine Karte anzeigen lassen, die alle Haltestationen mit zusätzliche Informationen anzeigt.
Ganz Deutschland anzuzeigen führt allerding zu Performanceproblemen, daher werden zunächst alle Marker ausgeblendet und können über den Filter je Bundesland hinzugeschaltet werden.
df.dropna(subset = ['latitude'], inplace=True)Dazu wird zuerst pro Bundesland eine FeatureGroup erstellt, die initial ausgeblendet ist.
state_dict = {}
for i in df.index:
state_dict.setdefault(df['state'][i], folium.FeatureGroup(name=df['state'][i], show=False, autoZIndex=False))
state_dict{'Nordrhein-Westfalen': <folium.map.FeatureGroup at 0x2303a3b6ce0>,
'Baden-Württemberg': <folium.map.FeatureGroup at 0x2303a3b6c80>,
'Bayern': <folium.map.FeatureGroup at 0x2303a3b62c0>,
'Niedersachsen': <folium.map.FeatureGroup at 0x230389aba00>,
'Sachsen': <folium.map.FeatureGroup at 0x230389ab940>,
'Schleswig-Holstein': <folium.map.FeatureGroup at 0x23039d75f30>,
'Berlin': <folium.map.FeatureGroup at 0x23037be0c10>,
'Brandenburg': <folium.map.FeatureGroup at 0x23039ab26e0>,
'Rheinland-Pfalz': <folium.map.FeatureGroup at 0x23039ab2890>,
'Hessen': <folium.map.FeatureGroup at 0x23039ab1f60>,
'Hamburg': <folium.map.FeatureGroup at 0x2303a509570>,
'Mecklenburg-Vorpommern': <folium.map.FeatureGroup at 0x2303a50a050>,
'Thüringen': <folium.map.FeatureGroup at 0x2303a509720>,
'Sachsen-Anhalt': <folium.map.FeatureGroup at 0x2303a509ab0>,
'Saarland': <folium.map.FeatureGroup at 0x2303a5095a0>,
'Schweiz CH': <folium.map.FeatureGroup at 0x2303a5091b0>,
'Bremen': <folium.map.FeatureGroup at 0x2303a508160>}
Per HTML kann ein Popup definiert werden, das erscheint, wenn man auf den Pin klickt. Hierbei wird das Bild und die zugehörigen Verkehrsverbünde und Zugtypen angezeigt.
Auf dieser Karte kann man sehr schön erkennen, wo die Bahnlienien verlaufen und dass es gewisse Regionen gibt, die nicht an die Bahn angeschlossen sind.
Beim Klicken durch die Bilder fällt auch auf, dass die Bahnhöfe häufig ältere Gebäude sind, von den allerdings viele Renoviert wurden.
Die roten Marker identifizieren Bahnhöfe in denen ICEs halten, in den gelben “nur noch” die IREs.
def GetIcon(availableTransports):
try:
if ('HIGH_SPEED_TRAIN' in availableTransports):
return folium.Icon(color='red', icon='map-marker')
elif ('INTERCITY_TRAIN' in availableTransports):
return folium.Icon(color='orange', icon='map-marker')
except:
return folium.Icon(color='blue', icon='map-marker')map_df = df
m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.
for i in map_df.index:
html=f"""
<img src="{map_df['image'][i]}" width="500px">
<br/>
<b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
<p>Transports: {map_df['availableTransports'][i]}</p>
<p>Associations: {map_df['transportAssociations'][i]}</p>
"""
parsedHtml = folium.Html(html, script=True)
popup = folium.Popup(parsedHtml, max_width=2650)
# this is probably done too often, but folium is smart enough
feature_group = state_dict[map_df['state'][i]]
m.add_child(feature_group)
folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIcon(map_df['availableTransports'][i]),
radius=8,
tooltip=map_df['name'][i],
popup=popup
).add_to(feature_group)
folium.LayerControl(collapsed=False).add_to(m)
mBahnhofsnahe Dienstleistungen
Mögliche Werte von Type aus der Dokumentation:
- INFORMATION_COUNTER [Informationsstand für Belange im Bahnhof (kein Fahrkartenverkauf)]
- TRAVEL_CENTER [Reisezentrum]
- VIDEO_TRAVEL_CENTER [Video Reisezentrum]
- TRIPLE_S_CENTER [3S Zentrale für Service, Sicherheit & Sauberkeit]
- TRAVEL_LOUNGE [Lounge (DB Lounge z.B.)]
- LOST_PROPERTY_OFFICE [Fundstelle]
- RAILWAY_MISSION [Bahnhofsmission]
- HANDICAPPED_TRAVELLER_SERVICE [Service für mobilitätseingeschränkte Reisende]
- LOCKER [Schließfächer]
- WIFI [WLan]
- CAR_PARKING [Autoparkplatz, ggf. kostenpflichtig]
- BICYCLE_PARKING [Fahrradparkplätze, ggf. kostenpflichtig]
- PUBLIC_RESTROOM [Öffentliches WC, ggf. kostenpflichtig]
- TRAVEL_NECESSITIES [Geschäft für den Reisendenbedarf]
- CAR_RENTAL [Car-Sharer oder Mietwagen]
- BICYCLE_RENTAL [Mieträder]
- TAXI_RANK [Taxi Stand]
- MOBILE_TRAVEL_SERVICE [Mobiler Service]
- RAD_PLUS (Rad+ Gebiet)
df_local_services = loadData('local_services.pkl')df_local_services.head()| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 0 | 1 | None | None | Mo-Su 06:15-22:30;PH 06:15-22:30 | MOBILE_TRAVEL_SERVICE | ||
| 1 | 1 | Duisburg Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 2 | 1 | None | None | None | RAILWAY_MISSION | ||
| 3 | 1 | None | Ja, um Voranmeldung unter 030 65 21 28 88 (Ort... | None | HANDICAPPED_TRAVELLER_SERVICE | ||
| 4 | 1 | None | None | None | LOCKER |
Man erkennt, dass die TRIPE_S_CENTER (Service, Sicerheit & Sauberkeit), TRAVEL_CENTER, TRAVEL_LOUNGE und VIDEO_TRAVEL_CENTER alle einen Namen, Öffnungszeiten und Geo-Koordinaten haben.
Die Services RAD_PLUS und TRAVEL_LOUNGE haben einen Namen, MOBILE_TRAVEL_SERVICE, INFORMATION_COUNTER und LOST_PROPERTY_OFFICE dafür Öffnungszeiten.
df_local_services.groupby(by='type').count().sort_values(by='id', ascending=False)| id | name | description | openingHours | latitude | longitude | |
|---|---|---|---|---|---|---|
| type | ||||||
| TRIPLE_S_CENTER | 4103 | 4103 | 0 | 4103 | 4103 | 4103 |
| CAR_PARKING | 3136 | 0 | 0 | 0 | 3136 | 3136 |
| BICYCLE_PARKING | 3081 | 0 | 0 | 0 | 3081 | 3081 |
| TAXI_RANK | 1100 | 0 | 0 | 0 | 1100 | 1100 |
| PUBLIC_RESTROOM | 562 | 0 | 0 | 0 | 562 | 562 |
| TRAVEL_NECESSITIES | 508 | 0 | 0 | 0 | 508 | 508 |
| HANDICAPPED_TRAVELLER_SERVICE | 318 | 0 | 233 | 0 | 318 | 318 |
| TRAVEL_CENTER | 260 | 260 | 260 | 260 | 260 | 260 |
| RAD_PLUS | 260 | 260 | 0 | 0 | 260 | 260 |
| LOCKER | 168 | 0 | 0 | 0 | 168 | 168 |
| MOBILE_TRAVEL_SERVICE | 127 | 0 | 0 | 127 | 127 | 127 |
| WIFI | 119 | 0 | 0 | 0 | 119 | 119 |
| RAILWAY_MISSION | 89 | 0 | 0 | 0 | 89 | 89 |
| VIDEO_TRAVEL_CENTER | 87 | 87 | 87 | 87 | 87 | 87 |
| INFORMATION_COUNTER | 76 | 0 | 0 | 76 | 76 | 76 |
| LOST_PROPERTY_OFFICE | 70 | 0 | 70 | 70 | 70 | 70 |
| CAR_RENTAL | 67 | 0 | 0 | 0 | 67 | 67 |
| TRAVEL_LOUNGE | 14 | 14 | 0 | 14 | 14 | 14 |
def getDataByType(type):
filtered_ids = df_local_services[df_local_services['type'] == type]['id']
count = filtered_ids.count()
unique_count = filtered_ids.nunique()
print(type,"Count:", count)
print(type,"unique Count:", unique_count)
return df_local_services[df_local_services['type'] == type]df_triples_center = getDataByType('TRIPLE_S_CENTER')TRIPLE_S_CENTER Count: 4103
TRIPLE_S_CENTER unique Count: 4103
Wenn man sich die Daten anschaut, tauchen dort viele IDs doppelt auf und es scheint, als wären die Daten schwierig zu interpretieren.
Gruppiert man die Daten allerdings anhand des Typs, sind die IDs eindeutig und können somit gut analysiert werden.
df_triples_center.head(3)| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 1 | 1 | Duisburg Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 13 | 1000 | Dresden | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 16 | 1002 | Frankfurt (Main) Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER |
Die Öffnungszeiten sind in einem bestimmten Format angegeben, daher müssen diese erst geparst werden.
Zuvor wird aber geprüft, ob es überhaupt abweichungen gibt.
df_triples_center['openingHours'].unique()array(['Mo-Su 00:00-24:00;PH 00:00-24:00'], dtype=object)
Scheinbar haben alle 3S-Center durchgehend geöffnet.
df_travel_center = getDataByType('TRAVEL_CENTER')TRAVEL_CENTER Count: 260
TRAVEL_CENTER unique Count: 257
df_travel_center.head(1)| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 11 | 1 | DB Reisezentrum Aachen Hbf | Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00 | 50.768944 | 6.0902 | TRAVEL_CENTER |
Bei den Reisezentren sehen die Öffnungszeiten schon interessanter aus.
df_travel_center['openingHours'].unique()array(['Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00',
'Mo-We 08:00-12:30,13:00-17:00;Th-Fr 08:00-12:30,13:00-18:00;Sa 08:00-13:30',
'Mo-Fr 08:00-17:00;Sa 08:00-13:00;Su 10:00-15:00',
'Mo 06:00-11:00,12:00-16:00;Tu-We,Fr 08:00-13:00,14:00-16:00;Th 09:00-13:00,14:00-19:00;Sa 08:00-12:00',
'Mo-Fr 06:30-12:00,13:00-18:30;Sa 08:00-13:00',
'Mo-Su 07:00-21:00',
'Mo-Fr 07:30-18:30;Sa-Su 09:00-13:00,13:30-17:00',
'Mo-Fr 09:00-12:00,13:00-17:25',
'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:30-13:30',
'Mo-Fr 08:00-18:00', 'Mo-Fr 07:00-19:00;Sa 09:00-14:00',
'Mo-Fr 07:00-11:30,12:00-14:30;Sa 08:30-13:30',
'Mo-Fr 06:30-09:00,09:30-17:30',
'Mo 06:30-18:30;Tu-Fr 07:30-18:30;Sa 06:30-11:30,12:00-14:30;Su 10:30-14:30,15:00-18:30',
'Mo-Fr 06:30-12:00,13:00-18:30', 'Mo-Fr 09:00-12:30,13:30-17:00',
'Mo 06:00-10:30,11:30-16:00;Tu 09:30-13:30,14:30-19:00;We-Fr 08:30-12:30,13:30-16:00;Sa 08:00-12:00',
'Mo-Fr 09:00-12:30,14:00-17:45',
'Mo 06:00-11:30,12:20-16:30;Tu-Fr 07:00-11:30,12:15-16:35',
'Mo-Fr 07:30-19:00;Sa-Su 09:00-18:00',
'Mo,Fr 06:15-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Tu-Th 06:15-12:00,12:45-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Sa 07:10-12:00 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. "',
'Mo-Fr 06:45-20:00;Sa-Su 07:00-20:00',
'Mo-Fr 08:00-18:00;Sa 09:00-16:00', 'Mo-Fr 06:30-11:45',
'Mo-Fr 07:00-18:00;Sa 08:00-12:00,12:30-14:30',
'Mo-Fr 07:00-21:00;Sa-Su 09:00-21:00',
'Mo 06:00-15:00,15:45-20:00;Tu-Fr 07:00-12:45,13:45-17:00;Sa-Su 09:30-15:00',
'Mo-We,Fr 08:30-13:00,13:45-18:00', 'Mo-Fr 06:30-17:30',
'Mo-Fr 05:45-18:45;Sa 08:00-13:00',
'Mo-Fr 07:30-12:30,13:30-17:30;Sa 07:30-12:30',
'Mo-Fr 08:00-13:00,14:00-17:45',
'Mo-Fr 07:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 08:30-18:30;Sa-Su 09:30-16:30',
'Mo 07:00-12:15,13:00-16:30;Tu-We,Fr 09:00-12:15,13:00-16:30;Th 09:00-12:15,13:00-18:00',
'Mo-Fr 07:30-19:00;Sa 08:15-15:00;Su 09:15-15:00',
'Mo-Fr 07:30-18:30;Sa 09:00-14:00',
'Mo 06:00-17:30;Tu-We,Fr 09:00-17:30;Th 09:00-20:00;Sa 08:00-16:30;Su 09:00-12:30,13:00-16:30',
'Mo-Fr 06:00-20:00;Sa 07:30-12:15,12:45-15:30',
'Mo-Fr 07:30-21:00;Sa-Su 08:30-18:30',
'Mo 06:30-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";Tu,Fr 07:00-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";We-Th 07:00-11:00,11:45-16:15 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!"',
'Mo-Fr 07:00-19:00;Sa 07:30-11:30,12:00-16:00;Su 09:30-15:00',
'Mo-Fr 06:00-20:30;Sa 07:00-14:30',
'Mo-Fr 07:30-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:30-15:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 11:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 07:00-11:30,13:30-18:00;Sa 09:00-14:00',
'Mo-Fr 07:00-20:00;Sa 08:30-19:00;Su 08:30-20:00',
'Mo-Fr 08:30-13:00,14:00-18:00;Sa 09:00-13:45',
'Mo 07:15-12:00,13:00-17:45;Tu-Fr 08:00-12:00,13:00-17:45;Sa 08:00-13:30',
'Mo-Fr 07:00-18:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum.";Sa 08:00-13:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum."',
'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:00-13:30',
'Mo-Fr 07:00-18:00;Sa 09:00-14:00',
'Mo-Fr 08:00-18:30;Sa 08:15-16:30;Su 12:00-17:00',
'Mo-Fr 07:00-19:00;Sa 09:00-14:30', 'Mo-Fr 07:15-17:15',
'Mo-Fr 08:30-17:30;Sa 08:30-13:30',
'Mo-Fr 07:45-12:00,13:30-18:00;Sa 08:10-13:45',
'Mo-Fr 09:00-18:00',
'Mo,Fr 07:00-18:00;Tu-Th 08:00-12:30,13:15-18:00;Sa 08:00-13:15;Su 10:00-15:15',
'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:00-12:30,13:30-17:00;Sa 07:00-12:30,13:00-15:00;Su 09:30-15:00',
'Mo-Fr 07:00-18:30;Sa 09:00-14:00',
'Mo-Fr 08:00-19:00;Sa 09:00-18:00;Su 10:00-16:00',
'Mo-Fr 07:45-12:30,13:30-17:45;Sa 09:00-14:00',
'Mo,Fr 06:00-18:00;Tu-Th 06:45-12:00,12:45-17:00;Su 09:00-12:30,13:00-17:00',
'Mo-Fr 07:00-19:00;Sa 09:00-17:00;Su 10:00-17:00',
'Mo-Fr 07:00-12:15,13:00-17:00;Sa 09:00-12:30,13:00-17:00;Su 08:00-12:30,13:00-15:00',
'Mo-Fr 08:00-12:30,13:30-17:00;Sa 10:00-15:30',
'Mo-Fr 07:15-12:15,13:00-17:30;Sa-Su 08:10-12:15,12:45-15:50',
'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch persönlich im DB Videoreisezentrum"',
'Mo-Fr 06:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Sa 07:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Su 08:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr"',
'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:15', 'Mo-Fr 07:30-18:30',
'Mo-Fr 07:40-18:30;Sa 08:00-13:30;Su 09:00-14:30',
'Mo-Fr 06:30-19:00;Sa 08:00-17:30;Su 10:00-18:00',
'Mo-Fr 07:00-19:00;Sa 09:30-14:30',
'Mo-Sa 07:00-20:00;Su 08:00-20:00',
'Mo-Fr 09:00-12:00,13:00-17:15',
'Mo-Fr 07:45-13:00,13:45-17:30;Sa 07:45-12:45',
'Mo-Fr 07:00-20:00;Sa-Su 09:00-19:00',
'Mo-Fr 07:45-12:30,13:30-18:00;Sa 08:45-14:20',
'Mo-Fr 07:30-18:30;Sa 08:30-15:00;Su 10:00-16:00',
'Mo 07:00-12:30,13:00-17:00;Tu,Fr 08:00-12:30,13:00-17:00;We-Th 08:00-12:30,13:00-18:00;Sa 07:30-12:30,13:00-15:00;Su 10:00-15:00',
'Mo,Th 06:30-18:00;Tu-We,Fr 06:30-12:00,13:00-17:00;Sa 06:30-12:00,13:00-15:00',
'Mo-Fr 06:50-18:00;Sa 08:30-13:00',
'Mo,Fr 06:45-18:00;Tu-Th 07:20-12:00,13:00-17:30',
'Mo-Th 08:00-12:00,12:45-16:30;Fr 08:00-13:30',
'Mo-We,Fr 07:30-11:30,12:30-17:30;Th 06:00-15:00,15:30-20:00;Sa 08:30-14:00;Su 10:00-15:00',
'Mo-Fr 09:00-13:00,14:00-17:30;Sa 09:00-13:00',
'Mo-Fr 07:45-12:00,12:45-17:30;Sa 08:00-13:00',
'Mo 06:00-20:00;Tu-Fr 07:00-20:00;Sa-Su 08:00-13:00,14:00-16:30',
'Mo-We,Fr 07:30-12:30,13:30-17:30;Th 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:30-12:45,13:45-18:00;Sa 08:30-13:00',
'Mo-Fr 08:00-12:00,13:00-16:30',
'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:30-12:30,13:30-17:30;Sa-Su 09:45-15:00',
'Mo-Fr 06:30-18:00;Sa 09:00-15:30;Su 11:00-17:00',
'Mo-Fr 08:00-13:00,13:30-17:00;Sa 08:00-13:00',
'Mo,Th-Fr 08:00-16:30;Tu-We 08:00-12:30,13:00-17:00;Sa 08:00-12:30,13:00-16:00',
'Mo,Th-Fr 07:15-11:45,12:00-17:00;Tu-We 07:15-11:45,12:30-17:00;Su 10:00-15:00',
'Mo 12:30-18:00,8:00-12:00;Tu-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
'Mo-Fr 07:00-19:00;Sa-Su 08:00-18:00',
'Mo-Fr 07:00-12:00,14:00-18:00;Sa 09:00-14:00',
'Mo-Fr 06:30-21:00;Sa 08:00-19:00;Su 09:00-20:00',
'Mo-Fr 08:00-12:30,13:30-18:00;Sa 09:00-13:00',
'Mo,We-Fr 07:00-12:45,13:45-17:00;Tu 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:00-12:00,12:45-17:00',
'Mo-Fr 07:00-19:30;Sa-Su 09:15-17:45',
'Mo-Fr 08:00-17:00;Sa 08:00-13:00',
'Mo-Fr 08:00-12:00,13:00-16:00',
'Mo-Fr 08:00-18:30;Sa 08:30-16:30',
'Mo-Fr 07:00-18:00;Sa 08:00-13:00',
'Mo-Fr 05:45-20:30;Sa-Su 06:45-20:00',
'Mo-Fr 08:00-13:00,14:00-18:00;Sa 08:30-13:00',
'Mo 06:00-19:30;Tu-Fr 07:00-19:30;Sa 08:00-17:00;Su 09:00-17:30',
'Mo-Fr 06:00-21:00;Sa-Su 07:00-21:00',
'Mo 06:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Tu-Th 07:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Fr 07:00-20:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Sa 09:00-17:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Su 10:00-18:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 "',
'Mo 07:30-12:00,12:45-16:30;Tu-Fr 08:30-12:00,12:45-16:30',
'Mo-Fr 07:45-12:15,13:00-17:45',
'Mo-Fr 09:00-12:00,13:00-17:25 open "Bitte Beachten: ;Am Montag, 31.07.23 geschlossen "',
'Mo-Fr 06:50-12:30,13:15-17:05;Sa 07:30-11:00,11:30-15:30;Su 08:45-12:00,12:30-17:15',
'Mo-Fr 08:00-18:00;Sa 09:00-13:00',
'Mo-Fr 08:00-12:00,13:00-17:00',
'Mo-Fr 06:00-18:00;Sa 07:00-14:00;Su 08:30-16:00',
'Mo-Fr 08:00-12:00,12:45-17:45;Sa 08:00-13:00',
'Mo-Fr 08:45-12:00,12:45-17:00',
'Mo-Fr 08:00-20:00;Sa 10:00-20:00;Su 10:00-18:00',
'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
'Mo 07:00-11:30,12:15-16:00;Tu-Fr 08:00-12:15,13:00-16:00',
'Mo-Fr 08:30-12:15,12:45-16:00',
'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 10:00-19:00',
'Mo-Fr 07:00-12:30,13:30-18:00;Sa 09:00-14:00',
'Mo-Fr 07:30-18:15 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Sa 08:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Su 10:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr"',
'Mo-Fr 08:30-13:00,14:00-18:00;Sa 08:30-13:00',
'Mo-Fr 05:45-20:00;Sa 06:30-19:30;Su 08:30-19:30',
'Mo-Fr 07:30-18:30 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum";Sa 09:00-14:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
'Mo-Fr 08:00-11:30,12:00-17:00;Sa-Su 08:00-11:30,12:00-15:30',
'Mo-Fr 07:30-18:30;Sa 07:30-11:30,12:00-15:30;Su 09:30-13:30,14:00-17:30',
'Mo-Fr 06:00-20:00;Sa 07:00-19:00;Su 08:00-20:00',
'Mo-Fr 06:00-21:15;Sa 07:00-19:00;Su 08:00-20:00',
'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:30',
'Mo-Fr 08:00-19:00;Sa 08:00-16:00;Su 09:00-16:00',
'Mo-Fr 08:00-12:15,12:45-16:00',
'Mo-Fr 06:45-18:35;Sa 07:30-13:00',
'Mo 06:00-10:30,11:30-16:00;Tu,Th-Fr 08:30-12:30,13:30-16:00;We 09:30-12:30,13:30-19:00;Sa 08:00-12:00',
'Mo-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
'24/7 closed "Dauerhaft geschlossen"', 'Mo-Fr 06:15-16:40',
'Mo-Fr 07:00-20:00;Sa-Su 09:00-18:00',
'Mo-Fr 06:50-20:00;Sa-Su 07:50-19:00',
'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 08:00-13:00,14:00-18:00',
'Mo-Fr 07:00-20:00;Sa-Su 08:00-18:00',
'Mo-Fr 08:00-12:00,13:00-16:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
'Mo-Fr 06:00-10:30,11:00-15:30,16:00-21:00;Sa-Su 06:45-15:30,16:00-19:45',
'Mo-We 08:00-12:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Th-Fr 13:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 07:00-17:00;Sa 08:00-13:00',
'Mo-Fr 09:00-13:15,13:45-17:00',
'Mo-Fr 07:00-18:30;Sa 08:00-12:00,12:30-16:00;Su 10:00-15:30',
'Mo,We-Fr 08:00-12:30,13:30-18:00;Tu 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:30-12:20,12:50-16:30',
'Mo-We,Fr 07:00-12:45,13:45-17:00;Th 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 06:15-12:00,12:45-16:00;Sa 07:45-12:45',
'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-13:30',
'Mo-Fr 08:30-12:30,14:00-17:00',
'Mo,Fr 06:30-19:15 open "gültig ab 01.01.22";Tu-Th 07:30-12:30,13:15-17:15 open "gültig ab 01.01.22";Su 10:45-14:00,14:30-19:00 open "gültig ab 01.01.22"',
'Mo-Fr 07:00-21:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 09:00-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 10:00-20:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 09:00-12:30,13:00-16:45', 'Mo-Fr 09:10-12:15,13:30-17:50',
'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 09:00-20:00',
'Mo-Fr 09:00-12:00,13:00-17:00 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
'Mo-Fr 09:00-12:30,13:00-18:00;Sa 09:00-13:00,13:30-16:00;Su 12:00-15:30,16:00-20:00',
'Mo-Fr 08:00-18:00;Sa 09:00-14:00',
'Mo-Fr 07:30-19:00;Sa 09:00-18:00;Su 10:00-18:30',
'Mo-Fr 06:30-18:00;Sa 08:00-17:00;Su 10:00-13:30',
'Mo-Fr 07:00-18:00;Sa-Su 10:00-15:15',
'Mo-Fr 08:30-12:00,12:45-16:15 open "Nutzen Sie auch das Video-Reisezentrum am Vorplatz"',
'Mo-Fr 07:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 09:00-13:00,13:30-17:00;Sa 08:00-13:00',
'Mo-Fr 06:15-12:00,12:35-16:50;Sa 08:30-14:00;Su 09:30-13:30',
'Mo-Th 08:00-18:00;Fr 08:00-13:00,14:00-18:00',
'Mo-Fr 10:00-16:00',
'Mo-Fr 08:00-12:30,13:15-18:00;Sa-Su 08:45-11:45,12:15-16:15',
'Mo-Fr 06:45-18:45;Sa 07:45-15:30',
'Mo-Fr 07:30-18:00;Sa 09:00-14:00;Su 09:30-13:30,14:00-17:00',
'Mo-Fr 08:15-12:15,13:00-17:45;Sa 07:15-12:30,13:00-15:45',
'Mo-Fr 07:30-18:30;Sa 08:30-14:00;Su 11:30-16:30',
'Mo 07:45-12:45,13:45-17:00;Tu,Th-Fr 07:00-12:45,13:45-17:00;We 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 07:30-13:00,14:00-18:00;Sa-Su 08:15-13:00,13:30-16:00',
'Mo-Fr 06:30-19:00;Sa 08:30-14:05',
'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-12:30',
'Mo-Fr 06:30-18:30;Sa 07:30-12:30;Su 11:30-16:30',
'Mo-Fr 08:00-20:00;Sa-Su 09:00-19:00',
'Mo-Fr 08:00-20:00;Sa-Su 10:00-20:00',
'Mo-Fr 07:00-18:00;Sa 08:30-16:00;Su 09:30-17:00',
'Mo-Fr 06:00-20:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 08:30-13:15,14:15-18:10;Sa 08:30-13:15',
'Mo-Th 07:15-12:00,12:30-15:45',
'Mo-Fr 06:00-21:15;Sa-Su 07:00-19:00',
'Mo-We,Fr 07:30-13:00,14:00-17:00;Th 06:00-15:00,15:30-20:00;Sa 07:30-11:00,11:30-15:00;Su 09:30-15:00',
'Mo-Fr 07:45-12:45,13:30-18:00;Sa-Su 08:30-12:30,13:00-15:15',
'Mo-Fr 08:00-18:00 open "Bitte beachten Sie unsere geänderten Öffnungszeit am ;21.07.23 von 08:00-12:30 und 13:00-16:00 Uhr; Wir bedienen Sie auch persönlich im DB Videoreisezentrum "',
'Mo-Fr 06:30-18:00 open "Wir danken für Ihr Verständnis";Sa 08:30-13:30 open "Wir danken für Ihr Verständnis"',
'Mo-Fr 08:30-11:30,12:30-16:55 open " Wir bedienen Sie auch persönlich im DB Videoreisezentrum ;am Bahnsteig 1. "',
'Mo-Fr 06:30-12:10,12:55-16:50;Sa 06:45-12:15',
'Mo-Fr 08:00-18:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 06:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 07:00-19:00;Sa 08:30-17:45;Su 09:30-18:00',
'Mo-Fr 08:50-12:30,13:45-17:30',
'Mo-Tu,Th-Fr 07:30-11:50,12:40-15:40',
'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 09:30-18:30',
'Mo-Fr 08:30-12:30,13:45-17:10',
'Mo-Fr 07:00-19:00;Sa 08:00-17:30;Su 10:00-15:30',
'Mo-Tu,Th-Fr 08:00-12:30,13:30-18:00;We 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:15-17:30;Sa 09:15-13:00',
'Mo-Fr 08:00-12:30,13:30-17:30;Sa 08:30-12:30',
'Mo 06:00-17:30;Tu-Fr 07:30-17:30;Sa 07:00-09:45,10:15-13:30;Su 10:00-17:30',
'Mo-Fr 06:15-20:15;Sa-Su 08:15-18:15',
'Mo-Fr 06:00-22:00;Sa-Su 07:00-22:00',
'Mo-Fr 08:00-18:00;Sa 09:00-18:00',
'Mo-Fr 07:00-18:30;Sa 08:30-14:00',
'Mo 08:15-12:30,13:00-16:15;Tu-Fr 08:00-12:30,13:00-15:30;Sa 08:15-13:15'],
dtype=object)
def getDays(daysList):
days_of_week = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
start_index = days_of_week.index(daysList[0])
end_index = days_of_week.index(daysList[-1])
if start_index <= end_index:
days_between = days_of_week[start_index:end_index+1]
else:
days_between = days_of_week[start_index:] + days_of_week[:end_index+1]
return days_betweendef get_minute_intervals(interval, time_range):
start_str, end_str = time_range.split('-')
start_time = datetime.strptime(start_str, '%H:%M')
end_time = datetime.strptime(end_str, '%H:%M')
interval = timedelta(minutes=interval)
current_time = start_time
intervals = []
while current_time <= end_time:
intervals.append(current_time.strftime('%H:%M'))
current_time += interval
return intervalsAktuell beinhalten die Daten nur die Öffnungszeiten pro Tag, ohne die genauen Uhrzeiten. Dazu müsste vermutlich eine doppelte x-Achse verwenden, um sowohl die Tag- als auch die Uhrzeiten anzuzeigen.
data_rows = []
for entry in df_travel_center['openingHours']:
day_time_pairs = entry.split(';')
row = {}
for pair in day_time_pairs:
try:
days, times = pair.split(' ')
days = days.split('-')
fixed_list = []
for item in days:
if (',' in item):
day_item = item.split(',')
fixed_list.append(day_item)
else:
fixed_list.append(item)
allDays = getDays(fixed_list)
#print(allDays)
for day in allDays:
row[day] = 1
except Exception as e:
#print(e)
#times = pair.split(' ')
#print(times)
row[day] = 0
data_rows.append(row)
df_opening_hours = pd.DataFrame(data_rows)
df_opening_hours| Mo | Tu | We | Th | Fr | Sa | Su | |
|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 2 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 3 | 0.0 | NaN | NaN | 1.0 | NaN | 1.0 | NaN |
| 4 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 255 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 256 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 257 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 258 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | NaN |
| 259 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
260 rows × 7 columns
Man kann sehen, dass in der Tabelle des öfteren NaN auftaucht. Wenn die Daten nicht verarbeitet werden konnten, lag es daran, dass dort noch Kommentare in den Zeiten vorhanden waren, wie beispielsweise “Aufgrund von Krankheit geschlossen”.
Daher wird davon ausgegangen, dass NaN ebenfalls geschlossen bedeutet.
df_opening_hours.replace(np.nan, 0, inplace=True)
df_opening_hours| Mo | Tu | We | Th | Fr | Sa | Su | |
|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 2 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 3 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 1.0 | 0.0 |
| 4 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 255 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 256 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 257 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 258 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 | 0.0 |
| 259 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
260 rows × 7 columns
Generell sieht man, dass die meisten Reisezentren am Wochenende geschlossen haben, vor allem am Sonntag. Vereinzelt sind aber auch unter der Woche Reisezentren geschlossen. Donnerstags scheint der Tag zu sein, an dem die meisten geöffnet haben.
Bei nährerer Betrachtung sieht man allerdings auch, dass große Knotenpunkte wie Mannheim, München, Hamburg oder Berlin durchgehend geöffnet haben. Hierbei ist die von Plotly mitgelieferte Zoom-Funktion hilfreich.
Leider wird dabei die Schriftgröße nicht ebenfalls gezoomt, daher ist die Anzeige der Y-Axe schwierig. Besser das Overlay nutzen.
custom_color_scale = [
[0.0, 'rgb(255, 0, 0)'], # closed = red
[1.0, 'rgb(0, 255, 0)'] # open = green
]
fig = go.Figure(data=go.Heatmap(
z=[[col for col in row] for _, row in df_opening_hours.iterrows()],
x=df_opening_hours.columns,
y=df_travel_center.iloc[df_opening_hours.index]['name'],
xgap=15,
ygap=1,
colorscale=custom_color_scale,
hoverongaps=False
#colorbar_thickness = 10
))
fig.update_layout(
title='Öffnungszeiten Reisezentren',
height=750
)
fig.update_xaxes(title_text='Wochentage',tickson='labels')
fig.update_yaxes(visible=False) # , tickfont = dict(size=4)
fig.update_traces(showscale=False)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Hier sieht man dies noch einmal deutlicher, allerdings sind die Werktage unter der Woche alle ziemlich gleich, sodass es sich auch um Ungenauigkeiten in den Daten handeln kann.
df_opening_hours_grouped = df_opening_hours.sum().sort_values()
px.bar(df_opening_hours_grouped)Unable to display output for mime type(s): application/vnd.plotly.v1+json
Nun können noch die Informationen der bahnhofsnahmen Dienstleistungen der Karte hinzugefügt werden.
Dazu müssen die Dataframes zunächst wieder zusammengebracht werden.
df_station_facilities_by_id = df_local_services.groupby('id')['type'].unique().reset_index()
df_station_facilities_by_id.head(1)| id | type | |
|---|---|---|
| 0 | 1 | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
df_station_facilities_by_id['id'] = df_station_facilities_by_id['id'].astype(int)# del map_df_extendedmap_df_extended = pd.merge(left=map_df, right=df_station_facilities_by_id, on=['id'], how='left')map_df_extended.head(1)| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | type | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.7678 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
Da in einigen Reihen von type NaN-Werte vorhanden sind, müssen diese zunächst ausgetauscht werden. Immer wenn ein Wert nicht vom Typ Numpy Array ist (also NaN), wird eine neue leere Liste erstellt, um keine Daten zu verlieren.
def replace_nan_with_empty_array(value):
if isinstance(value, np.ndarray):
return value
else:
return np.array([])map_df_extended['type'] = map_df_extended['type'].apply(replace_nan_with_empty_array)
map_df_extended| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | type | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.767800 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
| 1 | 2 | Aachen | DE | 48 | 50.770202 | 6.116475 | {} | Aachen-Rothe Erde | RB West | DB S&S | 52066 | Nordrhein-Westfalen | CATEGORY_4 | Beverstr. | [REGIONAL_TRAIN, BUS, CITY_TRAIN] | [AAV, VRS] | https://api.railway-stations.org/photos/de/2_1... | [CAR_PARKING, BICYCLE_PARKING, TRIPLE_S_CENTER... |
| 2 | 3 | Aachen | DE | 1 | 50.780360 | 6.070715 | {} | Aachen West | RB West | DB S&S | 52072 | Nordrhein-Westfalen | CATEGORY_5 | Republikplatz | [REGIONAL_TRAIN, BUS] | [AAV, VRS] | https://api.railway-stations.org/photos/de/3_1... | [TRIPLE_S_CENTER, CAR_PARKING, BICYCLE_PARKING... |
| 3 | 4 | Aalen | DE | 1 | 48.841013 | 10.096271 | {} | Aalen Hbf | RB Südwest | DB S&S | 73430 | Baden-Württemberg | CATEGORY_3 | Am Bahnhof | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS] | [OAM] | https://api.railway-stations.org/photos/de/4.jpg | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, CAR_P... |
| 4 | 5 | Abensberg | DE | 27 | 48.819456 | 11.846620 | {} | Abensberg | RB Süd | DB S&S | 93326 | Bayern | CATEGORY_6 | Bahnhofstr. | [REGIONAL_TRAIN] | [] | https://api.railway-stations.org/photos/de/5_1... | [TRIPLE_S_CENTER, PUBLIC_RESTROOM, TAXI_RANK, ... |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5716 | 8361 | Thalheim | DE | NaN | 50.696652 | 12.849032 | {} | Thalheim (Erzgeb) Mitte | Erzgebirgsbahn (EGB) | DB Regio-Netze | 09380 | Sachsen | CATEGORY_6 | Salzstraße | [REGIONAL_TRAIN, BUS] | [VMS] | NaN | [] |
| 5717 | 8363 | Herten | DE | 35 | 51.597508 | 7.139053 | {} | Herten (Westf) | RB West | DB S&S | 45699 | Nordrhein-Westfalen | CATEGORY_5 | Gartenstr. | [BUS, CITY_TRAIN] | [VRR] | NaN | [] |
| 5718 | 8375 | Rövershagen | DE | 41 | 54.160000 | 12.238300 | {} | Rövershagen Karls Erlebnisdorf (Purkshof) | RB Ost | DB S&S | 18182 | Mecklenburg-Vorpommern | CATEGORY_7 | Purkshof | [REGIONAL_TRAIN] | [VVW] | NaN | [] |
| 5719 | 8388 | Eutingen/Gäu | DE | 20 | 48.484700 | 8.753100 | {} | Eutingen Nord | RB Südwest | DB S&S | 72184 | Baden-Württemberg | CATEGORY_6 | Göttelinger Str. | [REGIONAL_TRAIN, BUS, CITY_TRAIN] | [VGF] | NaN | [] |
| 5720 | 8459 | Düsseldorf | DE | 120 | 51.278517 | 6.766979 | {} | Düsseldorf Flughafen Terminal | RB West | DB S&S | 40474 | Nordrhein-Westfalen | CATEGORY_4 | Flughafenstraße | [REGIONAL_TRAIN, CITY_TRAIN] | [VRS, VRR] | NaN | [] |
5721 rows × 18 columns
Jetzt wird wieder für jeden Typ eine FeatureGroup für die Karte erstellt.
for val in map_df_extended['type'][0]:
print(val)MOBILE_TRAVEL_SERVICE
TRIPLE_S_CENTER
RAILWAY_MISSION
HANDICAPPED_TRAVELLER_SERVICE
LOCKER
WIFI
CAR_PARKING
BICYCLE_PARKING
PUBLIC_RESTROOM
TRAVEL_NECESSITIES
TAXI_RANK
TRAVEL_CENTER
LOST_PROPERTY_OFFICE
facilities_dict = {}
for rowIndex in map_df_extended.index:
for listValue in map_df_extended['type'][rowIndex]:
facilities_dict.setdefault(listValue, folium.FeatureGroup(name=listValue, show=False, autoZIndex=False))
facilities_dict{'MOBILE_TRAVEL_SERVICE': <folium.map.FeatureGroup at 0x230416727d0>,
'TRIPLE_S_CENTER': <folium.map.FeatureGroup at 0x23041673250>,
'RAILWAY_MISSION': <folium.map.FeatureGroup at 0x23041672e30>,
'HANDICAPPED_TRAVELLER_SERVICE': <folium.map.FeatureGroup at 0x23041672d10>,
'LOCKER': <folium.map.FeatureGroup at 0x23041672e60>,
'WIFI': <folium.map.FeatureGroup at 0x23041672fb0>,
'CAR_PARKING': <folium.map.FeatureGroup at 0x23041672ef0>,
'BICYCLE_PARKING': <folium.map.FeatureGroup at 0x230416730a0>,
'PUBLIC_RESTROOM': <folium.map.FeatureGroup at 0x230416728c0>,
'TRAVEL_NECESSITIES': <folium.map.FeatureGroup at 0x23041673700>,
'TAXI_RANK': <folium.map.FeatureGroup at 0x230416736d0>,
'TRAVEL_CENTER': <folium.map.FeatureGroup at 0x230416736a0>,
'LOST_PROPERTY_OFFICE': <folium.map.FeatureGroup at 0x23041673670>,
'RAD_PLUS': <folium.map.FeatureGroup at 0x23041673640>,
'INFORMATION_COUNTER': <folium.map.FeatureGroup at 0x23041673610>,
'VIDEO_TRAVEL_CENTER': <folium.map.FeatureGroup at 0x23041673160>,
'CAR_RENTAL': <folium.map.FeatureGroup at 0x230416726b0>,
'TRAVEL_LOUNGE': <folium.map.FeatureGroup at 0x230416730d0>}
# NOTE: This requires python 3.10.1
def GetIconForFacilities(facility):
try:
match facility:
case 'MOBILE_TRAVEL_SERVICE':
return folium.Icon(color='lightblue', icon='map-marker')
case 'TRIPLE_S_CENTER':
return folium.Icon(color='red', icon='map-marker')
case 'RAILWAY_MISSION':
return folium.Icon(color='darkpurple', icon='map-marker')
case 'HANDICAPPED_TRAVELLER_SERVICE':
return folium.Icon(color='lightgray', icon='map-marker')
case 'CAR_PARKING':
return folium.Icon(color='gray', icon='map-marker')
case 'PUBLIC_RESTROOM':
return folium.Icon(color='cadetblue', icon='map-marker')
case 'TAXI_RANK':
return folium.Icon(color='beige', icon='map-marker')
case 'LOST_PROPERTY_OFFICE':
return folium.Icon(color='white', icon='map-marker')
case 'CAR_RENTAL':
return folium.Icon(color='black', icon='map-marker')
case _:
return folium.Icon(color='blue', icon='map-marker')
except:
return folium.Icon(color='blue', icon='map-marker')Letztlich werden beide Feature Groups, die von den Bundesländern und die der Einrichtungen zusammen auf die Karte gebracht.
Erst werden alle Marker den Feature Groups hinzugefügt, anschließen werden die Feature Groups der Karte hinzugefügt.
Dabei gibt es ein Problem, da sich aufgrund der gleichen Marker an der gleichen Lokation Überschneidungen ergeben.
Darüber hinaus kann das Laden je nach Computerressourcen länger dauern oder die Karte nicht korrekt gerendert werden.
Um diesem Problem entgegenzuwirken, kann das Folium Plugin MarkerCluster genutzt werden, welches nahe Marker gruppiert.
Zoomt man in die Karte, werden diese aufgelöst. Sind Marker auf der exakt gleichen Stelle, können durch ein Klick darauf alle Marker betrachtet werden.
Auf diese Weise lassen sich performant alle Marker gleichzeitig auf der Karte anzeigen.
Besonders viele und damit performance-intensive Werte sind TRIPE_S_CENTER, CAR_PARKING und BYICYLE_PARKING. Es gibt aber auch viele PUBLIC_RESTROOM, TRAVEL_NECESSITIES und TAXI_RANK.
map_df = map_df_extended
m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.
cluster = plugins.MarkerCluster(name='Deutschland')
cluster.add_to(m)
for i in map_df.index:
html=f"""
<img src="{map_df['image'][i]}" width="500px">
<br/>
<b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
<p>Transports: {map_df['availableTransports'][i]}</p>
<p>Associations: {map_df['transportAssociations'][i]}</p>
<p>Local services: {map_df['type'][i]}</p>
"""
parsedHtml = folium.Html(html, script=True)
popup = folium.Popup(parsedHtml, max_width=2650)
feature_group_state = state_dict[map_df['state'][i]]
marker = folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIcon(map_df['availableTransports'][i]),
radius=8,
tooltip=map_df['name'][i],
popup=popup
)
marker.add_to(feature_group_state)
marker.add_to(cluster)
for listValue in map_df_extended['type'][i]:
feature_group_type = facilities_dict[listValue]
marker = folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIconForFacilities(listValue),
radius=8,
tooltip=map_df['name'][i] + ' - ' + listValue
)
marker.add_to(feature_group_type)
marker.add_to(cluster)for fg in state_dict.values():
m.add_child(fg)
for fg in facilities_dict.values():
m.add_child(fg)
folium.LayerControl(collapsed=False).add_to(m)
m